/*!
 jQuery Wookmark plugin
 @name jquery.wookmark.js
 @author Christoph Ono (chri@sto.ph or @gbks)
 @author Sebastian Helzle (sebastian@helzle.net or @sebobo)
 @version 1.4.2
 @date 8/11/2013
 @category jQuery plugin
 @copyright (c) 2009-2013 Christoph Ono (www.wookmark.com)
 @license Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
 */
(function (factory) {
    if (typeof define === 'function' && define.amd)
        define(['jquery'], factory);
    else
        factory(jQuery);
}(function ($) {

    var Wookmark, defaultOptions, __bind;

    __bind = function(fn, me) {
        return function() {
            return fn.apply(me, arguments);
        };
    };

    // Wookmark default options
    defaultOptions = {
        align: 'center',
        autoResize: false,
        comparator: null,
        container: $('body'),
        ignoreInactiveItems: true,
        itemWidth: 0,
        fillEmptySpace: false,
        flexibleWidth: 0,
        offset: 2,
        onLayoutChanged: undefined,
        outerOffset: 0,
        resizeDelay: 50
    };

    Wookmark = (function() {

        function Wookmark(handler, options) {
            // Instance variables.
            this.handler = handler;
            this.columns = this.containerWidth = this.resizeTimer = null;
            this.activeItemCount = 0;
            this.direction = 'left';
            this.itemHeightsDirty = true;
            this.placeholders = [];

            $.extend(true, this, defaultOptions, options);

            // Bind instance methods
            this.update = __bind(this.update, this);
            this.onResize = __bind(this.onResize, this);
            this.onRefresh = __bind(this.onRefresh, this);
            this.getItemWidth = __bind(this.getItemWidth, this);
            this.layout = __bind(this.layout, this);
            this.layoutFull = __bind(this.layoutFull, this);
            this.layoutColumns = __bind(this.layoutColumns, this);
            this.filter = __bind(this.filter, this);
            this.clear = __bind(this.clear, this);
            this.getActiveItems = __bind(this.getActiveItems, this);
            this.refreshPlaceholders = __bind(this.refreshPlaceholders, this);
            this.sortElements = __bind(this.sortElements, this);

            // Collect filter data
            var i = 0, j = 0, filterClasses = {}, itemFilterClasses, $item, filterClass;

            for (; i < handler.length; i++) {
                $item = handler.eq(i);

                // Read filter classes
                itemFilterClasses = $item.data('filterClass');

                // Globally store each filter class as object and the fitting items in the array
                if (typeof itemFilterClasses == 'object' && itemFilterClasses.length > 0) {
                    for (j = 0; j < itemFilterClasses.length; j++) {
                        filterClass = $.trim(itemFilterClasses[j]).toLowerCase();

                        if (!(filterClass in filterClasses)) {
                            filterClasses[filterClass] = [];
                        }
                        filterClasses[filterClass].push($item[0]);
                    }
                }
            }
            this.filterClasses = filterClasses;

            // Listen to resize event if requested.
            if (this.autoResize)
                $(window).bind('resize.wookmark', this.onResize);

            this.container.bind('refreshWookmark', this.onRefresh);
        }

        // Method for updating the plugins options
        Wookmark.prototype.update = function(options) {
            this.itemHeightsDirty = true;
            $.extend(true, this, options);
        };

        // This timer ensures that layout is not continuously called as window is being dragged.
        Wookmark.prototype.onResize = function() {
            clearTimeout(this.resizeTimer);
            this.itemHeightsDirty = this.flexibleWidth !== 0;
            this.resizeTimer = setTimeout(this.layout, this.resizeDelay);
        };

        // Marks the items heights as dirty and does a relayout
        Wookmark.prototype.onRefresh = function() {
            this.itemHeightsDirty = true;
            this.layout();
        };

        /**
         * Filters the active items with the given string filters.
         * @param filters array of string
         * @param mode 'or' or 'and'
         */
        Wookmark.prototype.filter = function(filters, mode) {
            var activeFilters = [], activeFiltersLength, activeItems = $(),
                i, j, k, filter;

            filters = filters || [];
            mode = mode || 'or';

            if (filters.length) {
                // Collect active filters
                for (i = 0; i < filters.length; i++) {
                    filter = $.trim(filters[i].toLowerCase());
                    if (filter in this.filterClasses) {
                        activeFilters.push(this.filterClasses[filter]);
                    }
                }

                // Get items for active filters with the selected mode
                activeFiltersLength = activeFilters.length;
                if (mode == 'or' || activeFiltersLength == 1) {
                    // Set all items in all active filters active
                    for (i = 0; i < activeFiltersLength; i++) {
                        activeItems = activeItems.add(activeFilters[i]);
                    }
                } else if (mode == 'and') {
                    var shortestFilter = activeFilters[0],
                        itemValid = true, foundInFilter,
                        currentItem, currentFilter;

                    // Find shortest filter class
                    for (i = 1; i < activeFiltersLength; i++) {
                        if (activeFilters[i].length < shortestFilter.length) {
                            shortestFilter = activeFilters[i];
                        }
                    }

                    // Iterate over shortest filter and find elements in other filter classes
                    for (i = 0; i < shortestFilter.length; i++) {
                        currentItem = shortestFilter[i];
                        itemValid = true;

                        for (j = 0; j < activeFilters.length && itemValid; j++) {
                            currentFilter = activeFilters[j];
                            if (shortestFilter == currentFilter) continue;

                            // Search for current item in each active filter class
                            for (k = 0, foundInFilter = false; k < currentFilter.length && !foundInFilter; k++) {
                                foundInFilter = currentFilter[k] == currentItem;
                            }
                            itemValid &= foundInFilter;
                        }
                        if (itemValid)
                            activeItems.push(shortestFilter[i]);
                    }
                }
                // Hide inactive items
                this.handler.not(activeItems).addClass('inactive');
            } else {
                // Show all items if no filter is selected
                activeItems = this.handler;
            }

            // Show active items
            activeItems.removeClass('inactive');

            // Unset columns and refresh grid for a full layout
            this.columns = null;
            this.layout();
        };

        /**
         * Creates or updates existing placeholders to create columns of even height
         */
        Wookmark.prototype.refreshPlaceholders = function(columnWidth, sideOffset) {
            var i = this.placeholders.length,
                $placeholder, $lastColumnItem,
                columnsLength = this.columns.length, column,
                height, top, innerOffset,
                containerHeight = this.container.innerHeight();

            for (; i < columnsLength; i++) {
                $placeholder = $('<div class="wookmark-placeholder"/>').appendTo(this.container);
                this.placeholders.push($placeholder);
            }

            innerOffset = this.offset + parseInt(this.placeholders[0].css('borderWidth'), 10) * 2;

            for (i = 0; i < this.placeholders.length; i++) {
                $placeholder = this.placeholders[i];
                column = this.columns[i];

                if (i >= columnsLength || !column[column.length - 1]) {
                    $placeholder.css('display', 'none');
                } else {
                    $lastColumnItem = column[column.length - 1];
                    if (!$lastColumnItem) continue;
                    top = $lastColumnItem.data('wookmark-top') + $lastColumnItem.data('wookmark-height') + this.offset;
                    height = containerHeight - top - innerOffset;

                    $placeholder.css({
                        position: 'absolute',
                        display: height > 0 ? 'block' : 'none',
                        left: i * columnWidth + sideOffset,
                        top: top,
                        width: columnWidth - innerOffset,
                        height: height
                    });
                }
            }
        };

        // Method the get active items which are not disabled and visible
        Wookmark.prototype.getActiveItems = function() {
            return this.ignoreInactiveItems ? this.handler.not('.inactive') : this.handler;
        };

        // Method to get the standard item width
        Wookmark.prototype.getItemWidth = function() {
            var itemWidth = this.itemWidth,
                innerWidth = this.container.width() - 2 * this.outerOffset,
                firstElement = this.handler.eq(0),
                flexibleWidth = this.flexibleWidth;

            if (this.itemWidth === undefined || this.itemWidth === 0 && !this.flexibleWidth) {
                itemWidth = firstElement.outerWidth();
            }
            else if (typeof this.itemWidth == 'string' && this.itemWidth.indexOf('%') >= 0) {
                itemWidth = parseFloat(this.itemWidth) / 100 * innerWidth;
            }

            // Calculate flexible item width if option is set
            if (flexibleWidth) {
                if (typeof flexibleWidth == 'string' && flexibleWidth.indexOf('%') >= 0) {
                    flexibleWidth = parseFloat(flexibleWidth) / 100 * innerWidth;
                }

                var columns = ~~(0.5 + (innerWidth + this.offset) / (flexibleWidth + this.offset)),
                    columnWidth = Math.min(flexibleWidth, ~~((innerWidth - (columns - 1) * this.offset) / columns));

                itemWidth = Math.max(itemWidth, columnWidth);

                // Stretch items to fill calculated width
                this.handler.css('width', itemWidth);
            }

            return itemWidth;
        };

        // Main layout method.
        Wookmark.prototype.layout = function(force) {
            // Do nothing if container isn't visible
            if (!this.container.is(':visible')) return;

            // Calculate basic layout parameters.
            var columnWidth = this.getItemWidth() + this.offset,
                containerWidth = this.container.width(),
                innerWidth = containerWidth - 2 * this.outerOffset,
                columns = ~~((innerWidth + this.offset) / columnWidth),
                offset = 0, maxHeight = 0, i = 0,
                activeItems = this.getActiveItems(),
                activeItemsLength = activeItems.length,
                $item;

            // Cache item height
            if (this.itemHeightsDirty) {
                for (; i < activeItemsLength; i++) {
                    $item = activeItems.eq(i);
                    $item.data('wookmark-height', $item.outerHeight());
                }
                this.itemHeightsDirty = false;
            }

            // Use less columns if there are to few items
            columns = Math.max(1, Math.min(columns, activeItemsLength));

            // Calculate the offset based on the alignment of columns to the parent container
            offset = this.outerOffset;
            if (this.align == 'center') {
                offset += ~~(0.5 + (innerWidth - (columns * columnWidth - this.offset)) >> 1);
            }

            // Get direction for positioning
            this.direction = this.align == 'right' ? 'right' : 'left';

            // If container and column count hasn't changed, we can only update the columns.
            if (!force && this.columns !== null && this.columns.length == columns && this.activeItemCount == activeItemsLength) {
                maxHeight = this.layoutColumns(columnWidth, offset);
            } else {
                maxHeight = this.layoutFull(columnWidth, columns, offset);
            }
            this.activeItemCount = activeItemsLength;

            // Set container height to height of the grid.
            this.container.css('height', maxHeight);

            // Update placeholders
            if (this.fillEmptySpace) {
                this.refreshPlaceholders(columnWidth, offset);
            }

            if (this.onLayoutChanged !== undefined && typeof this.onLayoutChanged === 'function') {
                this.onLayoutChanged();
            }
        };

        /**
         * Sort elements with configurable comparator
         */
        Wookmark.prototype.sortElements = function(elements) {
            return typeof(this.comparator) === 'function' ? elements.sort(this.comparator) : elements;
        };

        /**
         * Perform a full layout update.
         */
        Wookmark.prototype.layoutFull = function(columnWidth, columns, offset) {
            var $item, i = 0, k = 0,
                activeItems = $.makeArray(this.getActiveItems()),
                length = activeItems.length,
                shortest = null, shortestIndex = null,
                itemCSS = {position: 'absolute'},
                sideOffset, heights = [],
                leftAligned = this.align == 'left' ? true : false;

            this.columns = [];

            // Sort elements before layouting
            activeItems = this.sortElements(activeItems);

            // Prepare arrays to store height of columns and items.
            while (heights.length < columns) {
                heights.push(this.outerOffset);
                this.columns.push([]);
            }

            // Loop over items.
            for (; i < length; i++ ) {
                $item = $(activeItems[i]);

                // Find the shortest column.
                shortest = heights[0];
                shortestIndex = 0;
                for (k = 0; k < columns; k++) {
                    if (heights[k] < shortest) {
                        shortest = heights[k];
                        shortestIndex = k;
                    }
                }

                // stick to left side if alignment is left and this is the first column
                sideOffset = offset;
                if (shortestIndex > 0 || !leftAligned)
                    sideOffset += shortestIndex * columnWidth;

                // Position the item.
                itemCSS[this.direction] = sideOffset;
                itemCSS.top = shortest;
                $item.css(itemCSS).data('wookmark-top', shortest);

                // Update column height and store item in shortest column
                heights[shortestIndex] += $item.data('wookmark-height') + this.offset;
                this.columns[shortestIndex].push($item);
            }

            // Return longest column
            return Math.max.apply(Math, heights);
        };

        /**
         * This layout method only updates the vertical position of the
         * existing column assignments.
         */
        Wookmark.prototype.layoutColumns = function(columnWidth, offset) {
            var heights = [],
                i = 0, k = 0, currentHeight,
                column, $item, itemCSS, sideOffset;

            for (; i < this.columns.length; i++) {
                heights.push(this.outerOffset);
                column = this.columns[i];
                sideOffset = i * columnWidth + offset;
                currentHeight = heights[i];

                for (k = 0; k < column.length; k++) {
                    $item = column[k];
                    itemCSS = {
                        top: currentHeight
                    };
                    itemCSS[this.direction] = sideOffset;

                    $item.css(itemCSS).data('wookmark-top', currentHeight);

                    currentHeight += $item.data('wookmark-height') + this.offset;
                }
                heights[i] = currentHeight;
            }

            // Return longest column
            return Math.max.apply(Math, heights);
        };

        /**
         * Clear event listeners and time outs.
         */
        Wookmark.prototype.clear = function() {
            clearTimeout(this.resizeTimer);
            $(window).unbind('resize.wookmark', this.onResize);
            this.container.unbind('refreshWookmark', this.onRefresh);
        };

        return Wookmark;
    })();

    $.fn.wookmark = function(options) {
        // Create a wookmark instance if not available
        if (!this.wookmarkInstance) {
            this.wookmarkInstance = new Wookmark(this, options || {});
        } else {
            this.wookmarkInstance.update(options || {});
        }

        // Apply layout
        this.wookmarkInstance.layout(true);

        // Display items (if hidden) and return jQuery object to maintain chainability
        return this.show();
    };
}));